package nl.utwente.eemcs.graphviz;

import java.util.ArrayList;
import java.util.List;

import nl.utwente.eemcs.graph.CompositeNode;
import nl.utwente.eemcs.graph.ConditionalConstruct;
import nl.utwente.eemcs.graph.Edge;
import nl.utwente.eemcs.graph.EdgeType;
import nl.utwente.eemcs.graph.Graph;
import nl.utwente.eemcs.graph.GraphCollaboration;
import nl.utwente.eemcs.graph.LoopConstruct;
import nl.utwente.eemcs.graph.Node;
import nl.utwente.eemcs.graph.ParallelConstruct;
import nl.utwente.eemcs.graph.Partition;
import nl.utwente.eemcs.graph.distribution.DistributionLocation;


public class NewGraphVisualizer {

	/**
	 * Generate a collaborative graph model, without communication edges between the processes
	 * @param graphs List with the processes
	 * @return
	 */
	public static String generateDot(GraphCollaboration collaboration) {
		NewGraphVisualizer visualizer = new NewGraphVisualizer();
		List<Edge> edges = new ArrayList<Edge>();
		edges.addAll(collaboration.getCommunicationEdges());
		edges.addAll(collaboration.getDataEdges());
		visualizer.drawCollaboration(collaboration.getProcesses(), edges);
		return visualizer.builder.toString();
	}
	
	public static String generateDot(Graph g) {
		NewGraphVisualizer visualizer = new NewGraphVisualizer();
		visualizer.drawProcess(g, "Process", false);
		return visualizer.builder.toString();
	}
	
	public static String generateDot(List<Graph> graphs) {
		NewGraphVisualizer visualizer = new NewGraphVisualizer();
		visualizer.drawCollaboration(graphs, new ArrayList<Edge>());
		return visualizer.builder.toString();
	}	
	
	private List<Node> drawnNodes;
	private List<Edge> drawnEdges;
	private StringBuilder builder;
	private int uniqueId = 0;
	
	private NewGraphVisualizer() {
		drawnNodes = new ArrayList<Node>();
		drawnEdges = new ArrayList<Edge>();
		builder = new StringBuilder();
	}
	
	private String drawCollaboration(List<Graph> processes, List<Edge> communicationEdges) {
		builder.append("digraph { \r\n");

		// Create processes
		for (Graph p : processes) 
			drawProcess(p, "process" + uniqueId++, true);
		
		// Draw edges
		for (Edge e : communicationEdges)
			drawEdge(e);

		builder.append("}\r\n");		

		return builder.toString();
	}

	private void drawProcess(Graph g, String name, boolean isSubProcess) {
		if (!isSubProcess)
			builder.append("digraph { \r\n");
		else {
			builder.append("subgraph cluster" + name.replace('.', '_') + " {\r\n");
			builder.append("color=lightgrey;\r\n");
		}
		
		// Draw nodes
		for (Node n: g.getNodes())
			drawNode(n);
		
		// Draw edges
		for (Edge e: g.getEdges())
			drawEdge(e);		

		if (isSubProcess)
			builder.append("label = \"" + name + "\";\r\n");
		
		builder.append("}\r\n");		
		
	}
	
	private void drawNode(Node n) {
		if (drawnNodes.contains(n))
			return;
		
		if (n instanceof ParallelConstruct)
			drawParallelConstruct((ParallelConstruct)n);
		else if (n instanceof ConditionalConstruct)
			drawConditionalConstruct((ConditionalConstruct)n);
		else if (n instanceof LoopConstruct)
			drawLoopConstruct((LoopConstruct)n);		
		else if (n instanceof Partition)
			drawPartition((Partition)n);
		else {
			// Draw a node (color shows distribution location)
			if (n.getLocation() == DistributionLocation.OnPremise)
				builder.append(getNodeName(n) + " [style=\"filled,solid\",color=black,fillcolor=white,fontsize=11];\r\n");
			else {
				builder.append(getNodeName(n) + " [style=\"filled,solid\",color=black,fillcolor=lightblue,fontsize=11];\r\n");			
			}
			drawnNodes.add(n);					
		}
		
		// Add node to drawn nodes
		drawnNodes.add(n);
	}

	private void drawEdge(Edge e) {
		if (drawnEdges.contains(e))
			return;
		
		// Draw start of the edge
		if (e.getStart() instanceof CompositeNode<?, ?>) {
			Node startNode = (Node)((CompositeNode<?,?>)e.getStart()).getEndNode();
			if (startNode instanceof CompositeNode<?, ?>)
				builder.append(getNodeName(((CompositeNode<?,?>)startNode).getEndNode()));		
			else
				builder.append(getNodeName(startNode));
		} else
			builder.append(getNodeName(e.getStart()));
		
		builder.append(" -> ");
		
		// Draw end of the edge
		if (e.getEnd() instanceof CompositeNode<?, ?>) {
			Node endNode = (Node)((CompositeNode<?,?>)e.getEnd()).getStartNode();
			if (endNode instanceof CompositeNode<?, ?>)
				builder.append(getNodeName(((CompositeNode<?,?>)endNode).getStartNode()));
			else
				builder.append(getNodeName(endNode));
		}
		else
			builder.append(getNodeName(e.getEnd()));		
		
		switch (e.getEdgeType()) {
			case Communication:
				builder.append("[style=dotted, label=\""+e.getLabel()+"\", fontsize=11]");
				break;
			case Data:
				builder.append("[style=dashed, color=red, fontcolor=red, label=\""+e.getLabel()+"\", fontsize=11]");
				break;			
			default:
				builder.append("[label=\""+e.getLabel()+"\", fontsize=11]");
				break;
		}
		builder.append(";");	

		// Add edge to drawn edges
		drawnEdges.add(e);
	}	
	
	/**
	 * Draw a parallel construct
	 * @param construct
	 */
	private void drawParallelConstruct(ParallelConstruct construct) {
		builder.append("subgraph cluster" + construct.getName() + " {\r\n");
		
		// Draw start and end node
		drawNode(construct.getStartNode());
		drawNode(construct.getEndNode());
		
		// Draw branches
		for (Graph branch : construct.getBranches()) {
			// Draw nodes
			for (Node n: branch.getNodes())
				drawNode(n);
			
			// Draw edges
			for (Edge e: branch.getEdges())
				drawEdge(e);
			
			// Draw edge between start and first node of the branch
			drawEdge(new Edge(construct.getStartNode(), branch.getStartNode(), EdgeType.Control));
			
			// Draw edge between end and the last node of the branch	
			drawEdge(new Edge(branch.getEndNode(), construct.getEndNode(), EdgeType.Control));
		}
		
		builder.append("label = \"" + construct.getName() + "\";\r\n");
		builder.append("graph[color=gray, fontcolor=gray];");
		builder.append("}\r\n");
	}	
	
	/**
	 * Draw a conditional construct
	 * @param construct
	 */
	private void drawConditionalConstruct(ConditionalConstruct construct) {
		builder.append("subgraph cluster" + construct.getName() + " {\r\n");
		
		// Draw start and end node
		drawNode(construct.getStartNode());
		drawNode(construct.getEndNode());
		
		Graph trueBranch = construct.getBranch(true);
		Graph falseBranch = construct.getBranch(false);
		
		if (trueBranch != null && trueBranch.getNodes().size() > 0) {
			// Draw nodes
			for (Node n: trueBranch.getNodes())
				drawNode(n);
			
			// Draw edges
			for (Edge e: trueBranch.getEdges())
				drawEdge(e);
			
			// Draw edge between start and first node of the branch
			drawEdge(new Edge(construct.getStartNode(), trueBranch.getStartNode(), EdgeType.Control, "true"));
			
			// Draw edge between end and the last node of the branch	
			drawEdge(new Edge(trueBranch.getEndNode(), construct.getEndNode(), EdgeType.Control));			
		} else
			drawEdge(new Edge(construct.getStartNode(), construct.getEndNode(), EdgeType.Control, "true"));	
		
		if (falseBranch != null && falseBranch.getNodes().size() > 0) {
			// Draw nodes
			for (Node n: falseBranch.getNodes())
				drawNode(n);
			
			// Draw edges
			for (Edge e: falseBranch.getEdges())
				drawEdge(e);
			
			// Draw edge between start and first node of the branch
			drawEdge(new Edge(construct.getStartNode(), falseBranch.getStartNode(), EdgeType.Control, "false"));
			
			// Draw edge between end and the last node of the branch	
			drawEdge(new Edge(falseBranch.getEndNode(), construct.getEndNode(), EdgeType.Control));			
		} else
			drawEdge(new Edge(construct.getStartNode(), construct.getEndNode(), EdgeType.Control, "false"));
		
		
		builder.append("label = \"" + construct.getName() + "\";\r\n");
		builder.append("graph[color=gray, fontcolor=gray];");
		builder.append("}\r\n");		
	}
	
	private void drawLoopConstruct(LoopConstruct construct) {
		builder.append("subgraph cluster" + construct.getName() + " {\r\n");		
		
		// Draw nodes
		for (Node n : construct.getInnerGraph().getNodes())
			drawNode(n);
		
		// Draw edges
		for (Edge e : construct.getInnerGraph().getEdges())
			drawEdge(e);		
		
		// Draw conditional node
		drawNode(construct.getConditionNode());
		
		if (construct.isEvaluateConditionBefore()) {
			drawEdge(new Edge(construct.getConditionNode(), construct.getInnerGraph().getStartNode(), EdgeType.Control, "loop"));
			drawEdge(new Edge( construct.getInnerGraph().getEndNode(), construct.getConditionNode(), EdgeType.Control));
		} else {
			drawEdge(new Edge(construct.getInnerGraph().getEndNode(), construct.getConditionNode(), EdgeType.Control));
			drawEdge(new Edge(construct.getConditionNode(), construct.getInnerGraph().getStartNode(), EdgeType.Control, "loop"));
		}

		builder.append("label = \"" + construct.getName() + "\";\r\n");
		builder.append("graph[color=gray, fontcolor=gray];");
		builder.append("}\r\n");			
	}	
	
	private void drawPartition(Partition construct) {
		builder.append("subgraph cluster" + construct.getName() + " {\r\n");
		
		// Draw nodes
		for (Node n: construct.getInnerGraph().getNodes())
			drawNode(n);
		
		// Draw edges
		for (Edge e: construct.getInnerGraph().getEdges())
			drawEdge(e);
		
		builder.append("label = \"" + construct.getName() + "\";\r\n");
		builder.append("graph[color=black, fontcolor=black];");
		builder.append("}\r\n");		
	}
	
	private String getNodeName(Node n) {
		if (n.hasAttribute("name"))
			return "\"" + n.getAttribute("name").toString().replaceAll("\"", "") + " (" + n.getName() + ")\"";
		else
			return n.getName();
	}
}